Skip to content

Open Service: Fix reactivity on deep signals, fire subscribers on load dependencies#34979

Merged
JReinhold merged 10 commits into
nextfrom
jeppe-cursor/docgen-subscription-referential-equality-5a81
Jun 3, 2026
Merged

Open Service: Fix reactivity on deep signals, fire subscribers on load dependencies#34979
JReinhold merged 10 commits into
nextfrom
jeppe-cursor/docgen-subscription-referential-equality-5a81

Conversation

@JReinhold
Copy link
Copy Markdown
Contributor

@JReinhold JReinhold commented May 29, 2026

Closes #

Works on #34824

What I did

Reworked the open-service reactive core to use per-field deep reactivity instead of one coarse state atom. This fixes docgen-style double emissions on re-navigation when data is unchanged, makes subscriptions precise, and makes a query's async load reactive to its synchronous dependencies.

State and writes

  • Replaced the single alien-signals atom + Immer with deepSignal (deepsignal) backed by @preact/signals-core. Reading ctx.self.state.<field> tracks only that field (including record keys added later).
  • setState((state) => …) mutates the proxy in place inside batch(); only changed fields invalidate dependents.

Subscriptions

  • Optional selector on subscribe (universal-store pattern): subscribe(input, selector, callback). The selector narrows reactive reads so unrelated sibling fields do not re-run the handler.
  • Value-based dedup at emit via es-toolkit isEqual. Emissions are detached plain values; whole-state snapshots use structuredClone, with JSON detach only for proxy slices from selectors.

Validation

  • Output validation runs at pull boundaries (query(), .loaded(), static build, subscription emit), not on the reactive path. Selector subscribers validate untracked so validation cannot broaden the tracked footprint.

Reactive load

  • For active subscriptions, a query's load re-runs when external signals it reads synchronously change (same-service fields or cross-service via getService). Loads are idempotent warming steps; one-shot side effects belong in commands. Superseded runs are epoch-gated so stale writes cannot clobber newer results. Direct query() / .loaded() remain one-shot.

Dependencies

  • Removed alien-signals; added deepsignal and @preact/signals-core.

Demonstrated behavior (docgen-style emulation: provider returns fresh-but-equal payloads on navigation):

  • Re-navigating to an already-loaded component emits once (no spurious second emission).
  • Extracting an unrelated record does not re-fire other subscribers.

Checklist for Contributors

Testing

The changes in this PR are covered in the following automated tests:

  • stories
  • unit tests
  • integration tests
  • end-to-end tests

Manual testing

Caution

This section is mandatory for all contributions. If you believe no manual test is necessary, please state so explicitly. Thanks!

No manual testing is required. Changes are confined to the internal open-service runtime under code/core/src/shared/open-service/. Coverage is in the Vitest suite (yarn test open-service), including fine-grained handler-spy tests, selector tests, equal-value dedup, and reactive load scenarios (same-service and cross-service deps, coalescing, superseding in-flight loads, non-subscription query() one-shot behavior).

Documentation

  • Add or update documentation reflecting your changes
  • If you are deprecating/removing a feature, make sure to update
    MIGRATION.MD

Updated code/core/src/shared/open-service/README.md (state/reactivity model, validation placement, load semantics, subscription flow including selector and reactive load).

Checklist for Maintainers

  • When this PR is ready for testing, make sure to add ci:normal, ci:merged or ci:daily GH label to it to run a specific set of sandboxes. The particular set of sandboxes can be found in code/lib/cli-storybook/src/sandbox-templates.ts

  • Make sure this PR contains one of the labels below:

    Available labels
    • bug: Internal changes that fixes incorrect behavior.
    • maintenance: User-facing maintenance tasks.
    • dependencies: Upgrading (sometimes downgrading) dependencies.
    • build: Internal-facing build tooling & test updates. Will not show up in release changelog.
    • cleanup: Minor cleanup style change. Will not show up in release changelog.
    • documentation: Documentation only changes. Will not show up in release changelog.
    • feature request: Introducing a new feature.
    • BREAKING CHANGE: Changes that break compatibility in some way with current major version.
    • other: Changes that don't fit in the above categories.

🦋 Canary release

This PR does not have a canary release associated. You can request a canary release of this pull request by mentioning the @storybookjs/core team here.

core team members can create a canary release here or locally with gh workflow run --repo storybookjs/storybook publish.yml --field pr=<PR_NUMBER>

Open in Web Open in Cursor 

Summary by CodeRabbit

Release Notes

  • New Features

    • Added selector support for query subscriptions to filter and derive selected values from results.
    • Introduced getStateSnapshot() method for retrieving detached state snapshots.
  • Documentation

    • Expanded Open Service documentation covering client architecture, reactive load behavior, and multi-master synchronization.

cursoragent and others added 2 commits May 29, 2026 21:24
…on re-emit

Co-authored-by: Jeppe Reinhold <JReinhold@users.noreply.github.com>
Re-engage alien-signals' identity-based dedup in subscribeToQuery by
memoizing the computed: return the previous reference when the new
handler output is deeply equal. Output-schema validation and Immer both
mint fresh references for unchanged data, which previously re-fired
subscribers on every state write (including writes to unrelated keys and
loads that rewrite a deeply-equal payload).

Co-authored-by: Jeppe Reinhold <JReinhold@users.noreply.github.com>
@cursor cursor Bot changed the title open-service: demonstrate referential-equality subscription re-emit open-service: dedup subscription emissions by value May 29, 2026
…te-only

Replace the single alien-signals state atom + Immer with a deepsignal
deep reactive proxy (backed by @preact/signals-core). State mutates in
place inside a batch; subscriptions track per-field reads so an unrelated
key/field write never re-runs a subscriber's handler.

Schemas now validate shape only and never convert: validation output is
discarded and the original reference is returned. A dev-only check throws
OpenServiceSchemaConversionError when a schema transforms/strips/coerces.

subscribe() gains an optional selector (universal-store pattern): the
callback receives the selected slice and fires only when it changes by
value. Emissions are deduped by value via es-toolkit isEqual; query
results and emissions are detached plain snapshots so the proxy never
escapes the runtime.
@cursor cursor Bot changed the title open-service: dedup subscription emissions by value open-service: fine-grained reactivity with deep signals (drop immer, validate-only) May 29, 2026
Output validation now runs only on pull boundaries (query()/.loaded()/
static build) and on whole-value subscription emissions, never narrowing
a selector subscriber's reactive footprint. Because validation no longer
runs where it would broaden tracking, schemas may transform/coerce again:
reverted the no-conversion contract and removed OpenServiceSchemaConversionError.

With a selector, the computed reads only the selected slice, so an
unselected sibling-field change no longer re-runs the handler (regression
guard added). Snapshotting: structuredClone for the plain whole-state
snapshot; JSON only to strip live proxy slices for selectors.

Renamed the docgen-flavored test fixture to concept-based naming
(rebuiltEqualValueOnLoadServiceDef) to match the existing convention.
@cursor cursor Bot changed the title open-service: fine-grained reactivity with deep signals (drop immer, validate-only) open-service: fine-grained reactivity with deep signals (drop immer) May 30, 2026
…es & tests

- Selector subscriptions now validate output too: validation runs
  untracked so it catches shape errors without broadening the reactive
  footprint (regression-guarded). Fixes output validation being skipped
  when a selector was passed.
- createServiceRuntime no longer clones initialState; the clone moves to
  registerService (the static build already clones), removing a double
  clone while still protecting a definition's shared initialState.
- Remove the false-positive 'different record' subscription test (covered
  better by the handler-spy fine-grained test) and merge the two selector
  tests into one emit+recompute test.
- Drop conversion framing from README/JSDoc; validation is described as it
  always behaved.
The draft naming was an Immer-ism; with the deep-signal proxy the callback
receives the live state, so name it accordingly across the type, fixtures,
docgen service, debug service, tests, and README.
A query's load now re-fires for active subscriptions whenever the
external signals it reads synchronously change (same-service or
cross-service via getService), turning it into a reactive async resource.
Implemented by running the load inside its own effect() so its reads are
ambiently tracked; loads with no external synchronous reads still fire
exactly once, so existing services are unaffected.

Superseding: each run carries an epoch and writes through epoch-gated
commands, so a slow stale load cannot overwrite a newer run's state.
Coalescing: changes batched together produce a single re-load. Direct
query()/.loaded() calls keep one-shot semantics. The load effect is torn
down with the subscription.

Loads are now contractually idempotent warming steps; one-shot side
effects belong in commands.
@JReinhold JReinhold self-assigned this Jun 1, 2026
@JReinhold JReinhold added maintenance User-facing maintenance tasks ci:normal Run our default set of CI jobs (choose this for most PRs). core labels Jun 1, 2026
@JReinhold JReinhold marked this pull request as ready for review June 1, 2026 09:09
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 1, 2026

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f4b4bef1-50e1-40b0-b74c-984c01de1b0a

📥 Commits

Reviewing files that changed from the base of the PR and between b58178b and 35c1415.

📒 Files selected for processing (1)
  • code/core/package.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • code/core/package.json

📝 Walkthrough

Walkthrough

This PR refactors the open-service state management system from alien-signals to deepsignal-backed reactivity. Core changes include replacing stateSignal() with getStateSnapshot(), adding selector-based subscriptions, moving output validation to wrapper boundaries, implementing reactive load gating, and systematically renaming setState callback parameters from draft to state across all service definitions, tests, and fixtures.

Changes

Open Service Deep Reactivity and State Refresh

Layer / File(s) Summary
Dependency updates and public type contracts
code/core/package.json, code/core/src/shared/open-service/types.ts
Dependencies switch from alien-signals to @preact/signals-core and deepsignal. Query.subscribe gains a selector-based overload; CommandSelf.setState parameter renamed draftstate; ServiceDefinition adds optional state schema; ServiceQueryRegistration simplified to single generic parameter.
Runtime deep reactive state and query execution
code/core/src/shared/open-service/service-runtime.ts
ServiceRuntime replaces stateSignal with getStateSnapshot() and optional stateSchema. Deep reactive proxy via deepSignal backs all state; createCommandSelf mutates proxy inside batch(); validation moves from runHandlerSync to wrapper boundaries; runReactiveLoad() gates stale writes via epochs; subscribeToQuery uses effect()/computed() with selector support, untracked validation, JSON-roundtrip detachment, and isEqual deduping.
Service registration, broadcast, and channel wiring
code/core/src/shared/open-service/service-registration.ts
registerService structurally clones initialState per registration, creates snapshot reconcilers, wraps commands via wrapCommandsForBroadcast, and optionally connects via connectRuntimeToChannel. clearRegistry calls disconnect() teardowns. applyRegistration selectively merges staticInputs without shallow-merging entire query objects.
README documentation and design rules
code/core/src/shared/open-service/README.md
Goals updated to deep-signal reactivity and Standard Schema validation. Load/query/subscription semantics clarified with dependency tracking, validation timing, and reactivity scoping. New "State and reactivity" and "Subscription Flow" sections document deep proxy semantics, batch mutation, selector behavior, and deduping. Client architecture, design rules, testing guidance, and agent notes expanded.
Service fixtures and docgen service updates
code/core/src/shared/open-service/fixtures.ts, code/core/src/shared/open-service/services/docgen/server.ts
setState callback parameter renamed draftstate in assignRecordField, preloadValue, writeLeftValue, writeRightValue handlers. New rebuiltEqualValueOnLoadServiceDef fixture for equality testing with fresh object allocations on each load. Docgen extractDocgen handler updated with parameter rename.
Type-level tests for runtime and registration
code/core/src/shared/open-service/index.test-d.ts, code/core/src/shared/open-service/server.test-d.ts
Type tests updated for setState parameter rename; registration typing adds staticInputs overrides and load-on-override rejection assertions; cross-service test restructured to independently define and register new service.
Service registration and static-build runtime tests
code/core/src/shared/open-service/service-registration.test.ts, code/core/src/shared/open-service/server.test.ts
service-registration.test.ts updates setState naming in increment/assignFromLookup/setValue handlers; restructures registration-only-static-build scenario to place load in definition and staticInputs at registration. server.test.ts updates static-load handlers with setState rename and reads snapshot via getStateSnapshot().
Reactive load and subscription coverage
code/core/src/shared/open-service/service-runtime.test.ts
Expands test coverage for selector validation, deeply-equal rebuild suppression, unrelated-field isolation, selector reruns, dependency tracking, cross-service reloads, infinite-loop avoidance, dependency coalescing, in-flight superseding, non-subscription non-reactivity, and self-contained load execution. Fixtures updated with setState parameter consistency.
Storybook debug service state mutations
code/.storybook/open-service-debug-service.ts
addActivity, syncStoryIndex, and recordPreloadVisit command handlers update setState callback parameter from draft to state while preserving state field assignments and activity logging.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • storybookjs/storybook#34860: Main PR is a direct follow-up that replaces the alien-signals/immer-style state signaling with deep-signal reactivity and updates service handler state mutation patterns.
  • storybookjs/storybook#34961: Both PRs modify service registration and runtime integration points in service-registration.ts and related server-side static-build code.
  • storybookjs/storybook#34954: Both PRs update the docgen open-service handler by renaming ctx.self.setState updater parameters in the extractDocgen command.

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@code/core/src/shared/open-service/service-runtime.test.ts`:
- Line 327: Replace fixed sleeps (e.g., the await new Promise(resolve =>
setTimeout(resolve, 50)) calls in service-runtime.test.ts) with deterministic
waits using the test runner's wait utilities (vi.waitFor or similar) that wait
for a concrete condition: check a spy/called count, assert the expected
length/content of the emitted array, or wait for a specific state change. Locate
the instances in the tests where those setTimeout sleeps are used and swap them
for vi.waitFor(() => expect(spy).toHaveBeenCalledTimes(n)) or vi.waitFor(() =>
expect(emitted).toEqual([...])) so the test completes when the observable/spy
reaches the expected state rather than after a fixed time.

In `@code/core/src/shared/open-service/service-runtime.ts`:
- Around line 1024-1027: The selector branch currently calls selector(output) on
raw handler output, bypassing schema validation/coercion; change it to run
validateQueryOutput(refs, queryName, queryDef, output) inside the existing
untracked call, capture the validated/coerced value (e.g., const validated =
validateQueryOutput(...)), then call selector(validated) and return
detachSnapshot(selector(validated)). Keep the untracked wrapper and existing
references (selector, untracked, validateQueryOutput, detachSnapshot, output,
queryName, queryDef, refs) so selector subscribers receive the same
validated/coerced TOutput as query()/ .loaded().
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 436b89f1-058f-47ba-a54b-14de09619f8a

📥 Commits

Reviewing files that changed from the base of the PR and between 88740ec and d0210c4.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (14)
  • code/.storybook/open-service-debug-service.ts
  • code/core/package.json
  • code/core/src/shared/open-service/README.md
  • code/core/src/shared/open-service/fixtures.ts
  • code/core/src/shared/open-service/index.test-d.ts
  • code/core/src/shared/open-service/server.test-d.ts
  • code/core/src/shared/open-service/server.test.ts
  • code/core/src/shared/open-service/server.ts
  • code/core/src/shared/open-service/service-registration.test.ts
  • code/core/src/shared/open-service/service-registration.ts
  • code/core/src/shared/open-service/service-runtime.test.ts
  • code/core/src/shared/open-service/service-runtime.ts
  • code/core/src/shared/open-service/services/docgen/server.ts
  • code/core/src/shared/open-service/types.ts

Comment thread code/core/src/shared/open-service/service-runtime.test.ts
Comment thread code/core/src/shared/open-service/service-runtime.ts
@storybook-app-bot
Copy link
Copy Markdown

storybook-app-bot Bot commented Jun 1, 2026

Package Benchmarks

Commit: 35c1415, ran on 3 June 2026 at 09:31:52 UTC

The following packages have significant changes to their size or dependencies:

storybook

Before After Difference
Dependency count 72 74 🚨 +2 🚨
Self size 20.42 MB 20.40 MB 🎉 -25 KB 🎉
Dependency size 36.11 MB 36.65 MB 🚨 +539 KB 🚨
Bundle Size Analyzer Link Link

@storybook/cli

Before After Difference
Dependency count 203 205 🚨 +2 🚨
Self size 908 KB 908 KB 0 B
Dependency size 88.61 MB 89.13 MB 🚨 +513 KB 🚨
Bundle Size Analyzer Link Link

@storybook/codemod

Before After Difference
Dependency count 196 198 🚨 +2 🚨
Self size 32 KB 32 KB 🚨 +36 B 🚨
Dependency size 87.10 MB 87.62 MB 🚨 +513 KB 🚨
Bundle Size Analyzer Link Link

create-storybook

Before After Difference
Dependency count 73 75 🚨 +2 🚨
Self size 1.08 MB 1.08 MB 0 B
Dependency size 56.53 MB 57.05 MB 🚨 +513 KB 🚨
Bundle Size Analyzer node node

…orybookjs/storybook into jeppe-cursor/docgen-subscription-referential-equality-5a81

# Conflicts:
#	code/core/src/shared/open-service/service-runtime.test.ts
#	code/core/src/shared/open-service/service-runtime.ts
@JReinhold JReinhold changed the title open-service: fine-grained reactivity with deep signals (drop immer) Open Service: Fix reactivity on deep signals, fire subscribers on load dependencies Jun 1, 2026
Selector subscribers now receive schema-validated slices. A tracked
selector(output) read preserves fine-grained proxy dependencies because
validation returns a plain parsed value.
@JReinhold JReinhold requested a review from ndelangen June 1, 2026 12:27
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
code/core/src/shared/open-service/README.md (1)

382-382: ⚡ Quick win

Add language specifiers to fenced code blocks.

Two fenced code blocks are missing language specifiers. Add identifiers to improve markdown rendering and enable syntax highlighting where applicable.

📝 Proposed fix

Line 382 (state sync diagram):

-```
+```text
 ┌─────────────────────────┐     channel (services:*)     ┌─────────────────────────┐
 │  Manager process        │  ◄────────────────────────►  │  Preview process        │

Line 429 (state sync sequence):

-```
+```text
 Peer A (manager)            Channel              Peer B (preview)
 ─────────────────────────────────────────────────────────────────

Also applies to: 429-429

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@code/core/src/shared/open-service/README.md` at line 382, Add language
specifiers to the two fenced code blocks in the README: the "state sync diagram"
block that begins with the Manager/Preview ASCII diagram and the "state sync
sequence" block that begins with "Peer A (manager)            Channel           
Peer B (preview)"; update their opening ``` fences to include a language
identifier (e.g., ```text or ```diff) so Markdown renderers apply syntax
highlighting and preserve formatting.
code/e2e-tests/open-service-background.spec.ts (1)

36-55: ⚡ Quick win

Prefer web-first waits over fixed waitForTimeout(5_000).

The two hard 5s sleeps are a Playwright anti-pattern: they add unconditional latency yet can still race on slow CI. The expect(darkSwatch).toBeVisible() and .poll(...) calls already wait deterministically, so the explicit sleeps can be dropped (or replaced with a concrete readiness condition such as waiting for the iframe load / a preview element).

♻️ Proposed change
     await page.goto(`${storybookUrl}/?path=${STORY_PATH}`);
     await page.waitForSelector('`#storybook-preview-iframe`');
-    await page.waitForTimeout(5_000);

     const darkSwatch = page.getByRole('toolbar').getByRole('button', { name: 'Dark', exact: true });

     await expect(darkSwatch).toBeVisible();
     await darkSwatch.click();
@@
     await page.reload();
     await page.waitForSelector('`#storybook-preview-iframe`');
-    await page.waitForTimeout(5_000);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@code/e2e-tests/open-service-background.spec.ts` around lines 36 - 55, Remove
the two fixed page.waitForTimeout(5_000) sleeps and replace them with web-first
waits: after goto and after page.reload, wait for the preview iframe to finish
loading (e.g., await
page.frameLocator('`#storybook-preview-iframe`').locator('body').waitFor() or use
an explicit wait for the previewBodyBackground readiness) and then reuse the
existing expect.poll(() => previewBodyBackground(page), ...) checks; keep the
darkSwatch visibility/assertions and ensure previewBodyBackground is polled
after the iframe load so the test no longer relies on hard sleeps.
code/core/src/shared/open-service/service-sync.ts (1)

73-75: 💤 Low value

isPlainObject may incorrectly accept class instances or objects with modified prototypes.

The check typeof value === 'object' && value !== null && !Array.isArray(value) passes for class instances, Object.create(null) objects, and objects with non-Object prototypes. For untrusted channel payloads, consider checking Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null to ensure only true plain objects are accepted.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@code/core/src/shared/open-service/service-sync.ts` around lines 73 - 75, The
current isPlainObject function accepts class instances and objects with modified
prototypes; change its implementation to verify the object's prototype is either
Object.prototype or null by using Object.getPrototypeOf(value) ===
Object.prototype || Object.getPrototypeOf(value) === null in addition to the
existing typeof/value/null/Array checks so only true plain objects (including
Object.create(null)) pass; keep the function name isPlainObject and its type
predicate signature when updating the logic.
code/core/src/channels/postmessage/index.ts (1)

233-239: 💤 Low value

Potential redundant flush when both paths trigger.

The new flush at line 238 doesn't check this.connected, while the existing flush in setHandler (lines 53-56) sets connected = true to prevent re-flushing. If setHandler's flush already ran (setting connected = true), this new flush will still execute on every incoming message while buffer.length > 0, though flush() itself clears the buffer so subsequent calls are no-ops.

The logic is safe (buffer is cleared), but consider guarding with !this.connected for clarity and to avoid the extra function call:

-        if (this.config.page === 'manager' && this.buffer.length) {
+        if (this.config.page === 'manager' && this.buffer.length && !this.connected) {
           this.flush();
+          this.connected = true;
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@code/core/src/channels/postmessage/index.ts` around lines 233 - 239, The
preview postMessage flush can redundantly call flush() even after setHandler has
already flushed and set this.connected; update the conditional in the
postMessage handling block (the code that currently checks this.config.page ===
'manager' && this.buffer.length) to also require !this.connected before calling
this.flush(), so it only flushes when the channel hasn't already been marked
connected by the setHandler path (referencing this.connected, setHandler, and
flush).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@code/.storybook/services-preset.ts`:
- Around line 6-12: Update the file-level comment above the services preset to
stop implying the background example is gated by STORYBOOK_OPEN_SERVICE_DEBUG;
state that registerBackgroundService() runs unconditionally and only the debug
service is enabled by the STORYBOOK_OPEN_SERVICE_DEBUG flag (reference the
services function and the registerBackgroundService call), so readers know the
env var only controls the debug service.

---

Nitpick comments:
In `@code/core/src/channels/postmessage/index.ts`:
- Around line 233-239: The preview postMessage flush can redundantly call
flush() even after setHandler has already flushed and set this.connected; update
the conditional in the postMessage handling block (the code that currently
checks this.config.page === 'manager' && this.buffer.length) to also require
!this.connected before calling this.flush(), so it only flushes when the channel
hasn't already been marked connected by the setHandler path (referencing
this.connected, setHandler, and flush).

In `@code/core/src/shared/open-service/README.md`:
- Line 382: Add language specifiers to the two fenced code blocks in the README:
the "state sync diagram" block that begins with the Manager/Preview ASCII
diagram and the "state sync sequence" block that begins with "Peer A (manager)  
Channel              Peer B (preview)"; update their opening ``` fences to
include a language identifier (e.g., ```text or ```diff) so Markdown renderers
apply syntax highlighting and preserve formatting.

In `@code/core/src/shared/open-service/service-sync.ts`:
- Around line 73-75: The current isPlainObject function accepts class instances
and objects with modified prototypes; change its implementation to verify the
object's prototype is either Object.prototype or null by using
Object.getPrototypeOf(value) === Object.prototype ||
Object.getPrototypeOf(value) === null in addition to the existing
typeof/value/null/Array checks so only true plain objects (including
Object.create(null)) pass; keep the function name isPlainObject and its type
predicate signature when updating the logic.

In `@code/e2e-tests/open-service-background.spec.ts`:
- Around line 36-55: Remove the two fixed page.waitForTimeout(5_000) sleeps and
replace them with web-first waits: after goto and after page.reload, wait for
the preview iframe to finish loading (e.g., await
page.frameLocator('`#storybook-preview-iframe`').locator('body').waitFor() or use
an explicit wait for the previewBodyBackground readiness) and then reuse the
existing expect.poll(() => previewBodyBackground(page), ...) checks; keep the
darkSwatch visibility/assertions and ensure previewBodyBackground is polled
after the iframe load so the test no longer relies on hard sleeps.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 20900f7a-7e0c-4cbd-9ed4-24c412b10d38

📥 Commits

Reviewing files that changed from the base of the PR and between 8715ba8 and b58178b.

📒 Files selected for processing (36)
  • code/.storybook/background-service/definition.ts
  • code/.storybook/background-service/manager.tsx
  • code/.storybook/background-service/preview.ts
  • code/.storybook/background-service/server.ts
  • code/.storybook/manager.tsx
  • code/.storybook/preview.tsx
  • code/.storybook/services-preset.ts
  • code/core/src/channels/postmessage/index.ts
  • code/core/src/core-server/presets/common-preset.ts
  • code/core/src/manager-api/modules/provider.ts
  • code/core/src/manager-api/root.tsx
  • code/core/src/shared/open-service/README.md
  • code/core/src/shared/open-service/client.ts
  • code/core/src/shared/open-service/fixtures.ts
  • code/core/src/shared/open-service/manager.ts
  • code/core/src/shared/open-service/preview.ts
  • code/core/src/shared/open-service/server.test-d.ts
  • code/core/src/shared/open-service/service-channel.ts
  • code/core/src/shared/open-service/service-client.test.ts
  • code/core/src/shared/open-service/service-client.ts
  • code/core/src/shared/open-service/service-definition.ts
  • code/core/src/shared/open-service/service-registration-sync.test.ts
  • code/core/src/shared/open-service/service-registration.test.ts
  • code/core/src/shared/open-service/service-registration.ts
  • code/core/src/shared/open-service/service-runtime.ts
  • code/core/src/shared/open-service/service-sync.ts
  • code/core/src/shared/open-service/service-transport.ts
  • code/core/src/shared/open-service/services/docgen/definition.ts
  • code/core/src/shared/open-service/services/docgen/server.ts
  • code/core/src/shared/open-service/types.ts
  • code/core/src/shared/open-service/use-service-command.test.tsx
  • code/core/src/shared/open-service/use-service-command.ts
  • code/core/src/shared/open-service/use-service-query.test.tsx
  • code/core/src/shared/open-service/use-service-query.ts
  • code/e2e-tests/open-service-background.spec.ts
  • code/playwright.config.ts
✅ Files skipped from review due to trivial changes (3)
  • code/.storybook/background-service/definition.ts
  • code/.storybook/manager.tsx
  • code/core/src/shared/open-service/client.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • code/core/src/shared/open-service/service-runtime.ts
  • code/core/src/shared/open-service/fixtures.ts

Comment thread code/.storybook/services-preset.ts Outdated
Base automatically changed from claude/wizardly-bartik-cc2705 to next June 3, 2026 07:30
@ndelangen ndelangen force-pushed the jeppe-cursor/docgen-subscription-referential-equality-5a81 branch from 768479d to 8715ba8 Compare June 3, 2026 07:46
@JReinhold JReinhold added the qa:skip Pull Requests that do not need any QA. label Jun 3, 2026
@JReinhold JReinhold merged commit 6ed450a into next Jun 3, 2026
141 of 146 checks passed
@JReinhold JReinhold deleted the jeppe-cursor/docgen-subscription-referential-equality-5a81 branch June 3, 2026 11:17
@github-actions github-actions Bot mentioned this pull request Jun 3, 2026
20 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci:normal Run our default set of CI jobs (choose this for most PRs). core maintenance User-facing maintenance tasks qa:skip Pull Requests that do not need any QA.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants